This document describes how
valiation works in Stripes, how you specify validations, and how
ActionBeans interact with validation. It includes the following
sections:
Overview
Validation is conceptually broken into three areas in Stripes:
- Annotation Driven Validation
- Type Converstion
- Custom Validation
Annotation
Driven Validation is named thusly because the way the validation rules
are specified is through annotations in the ActionBean. These
Validations apply to single fields in the form/ActionBean, and
encapsulate common and often used validations.
Type Conversion is
the process of converting a single form field input from the String
value in the HttpServletRequest into a Java object (of any Class,
including String). Type conversion is a core part of the validation
process, and is easy to extend to new or custom types.
Custom
Validation is the name given to the validation performed by arbitrary
Java code in one or more validation methods in an ActionBean. An
ActionBean can define zero, one or many validation methods and apply
them in different scenarios.
Validation Processing Flow
Given
the different kinds of validation on offer, one might ask how they are
applied, and what happens when errors are produced at any stage. The
following is the general flow:
- Required field validation is run first
- For fields that have no errors after required field validation
- Perform minimum/maximum length and mask validations
- If the field still has no errors, convert to the target type and bind the property to the ActionBean
- If the field converted without errors, run any min/max value checks (for numbers)
- For fields that have no errors, evaluate expression based validations
- If there are still no errors, run custom validation (unless overridden to run with errors)
This
strategy is designed to catch the largest possible number of validation
errors possible in a single pass. Thus if the user forgets to enter one
value, and mis-types another, they will be presented with both errors
the first time they submit the form - but they will not be presented
with any additional (e.g. minimum length violation) errors for the field
that was required but not supplied. Lastly it should be noted that the
reason custom validation is run only when other validations succeed is
so that it can rely on the ActionBean being in a well defined. This is,
however, configurable (see the Configuration Reference).
As we will see in the sections Selectively Disabling Validation and Changing Validation Outcomes this is not the whole story, but it's good enough for now.
Validation Error Classes
Before
we go much further, a quick tour of the different validation error
classes is in order. We'll be seeing a lot about validation errors in
the next few sections, so it is worthwhile to understand the different
types available, and how they are used.
ValidationError
is an interface that specifies the methods that all validation error
types must support. This makes it easy to have multiple different error
classes that offer different functionality. It also means that you can
write your own error classes easily if you are not satisfied with the
ones that Stripes supplies (hopefully that won't be the case).
SimpleError is, as it says, a simple implementation or ValidationError.
It allows a hard-coded message (or one supplied at construction time)
to be used, and provides the ability to insert the name and value of the
field (and other arbitrary bits of information) into the message.
LocalizableError extends SimpleError
with the capability to look up the error message in an external
resource bundle. The name of the bundle can be configured - to do this
take a look at the Configuration Reference.
Looks twice, once with the key prefixed with the action path of the
ActionBean being used, and once with the exact key provided.
ScopedLocalizableError extends LocalizableError
and provides the ability to perform "scoped searches" for an error
message. In reality this just means looking for error messages in the
resource bundle with names corresponding to:
<actionPath>.<fieldName>.<errorName><actionPath>.<fieldName>.errorMessage<fieldName>.<errorName><fieldName>.errorMessage<actionPath>.<errorName><defaultScope>.<errorName>
In the descriptions below you will see reference to "default scope" and "error name", which refer to how a ScopedLocalizableError is found. All the built in validations in Stripes use ScopedLocalizableError, so that you can specify fairly generic messages at a high level and override them as you see fit.
All of the error messages in Stripes use java.text.MessageFormat
which allows you to provide parameters to be merged in to the message.
The first two parameters (0 and 1 respectively) are always the field
name and field value respectively. An example follows, but check the
individual error class javadocs, and the javadoc for MessageFormat for more details:
myCustomError={0} must be formatted mm/dd/yy. {1} is not a valid date.
|
Annotation Driven Validation
There are two annotations for adding validations to fields. The @Validate annotation is used to specify validations for scalar or simple properties within an ActionBean. The @ValidateNestedProperties annotation is used to aggregate one or more @Validate
annotations in order to specify validations for nested properties of
complex types. The annotations can be attached to either the getter or
setter methods on the ActionBean. They can also be attached directly to
the properties (even private ones).
Each @Validate can specify multiple validations for a single property. The following example demonstrates how to use the annotations:
@Validate(required=true, minvalue=13, maxvalue=120)
private int age;
@ValidateNestedProperties({
@Validate(field="line1", required=true, minlength=5, maxlength=50),
@Validate(field="line2", minlength=5, maxlength=50),
@Validate(field="zip", required=true, mask="\\d{5}(-\\d{4})?")
})
public Address getAddress() { ... }
|
The following sections walk through the syntax of each validation that can be specified with the @Validate annotation.
@Validate(field="...")
The field property is used only when the @Validate annotation is nested within a @ValidateNestedProperties annotation. It is used to specify the property of the complex type to which the validation applies.
@Validate(required=true/false)
Specifies
whether or not the annotated field is required when the form is
submitted. Defaults to false (so it's never necessary to write
required=false even though you can). If a required field is not supplied
a single error is generated. The "default scope" is specified as validation.required and the error name is valueNotPresent. This results in an attempt to find error messages in the resource bundle with the following keys:
- actionPath.fieldName.valueNotPresent
- actionPath.fieldName.errorMessage
- fieldName.valueNotPresent
- fieldName.errorMessage
- actionPath.valueNotPresent
- validation.required.valueNotPresent
@Validate(on="...")
The 'on' attribute of the @Validate annotation does not perfom any validation itself, but restricts the application of the required check. To reiterate: it modifies when the required check is applied - not other validations specified at the same time.
By default required field validation is applied to all events not marked with @DontValidate. However, by specifying one or more event names in the 'on' attribute it can be restricted to just those events. For example:
@Validate(required=true, on={"save", "update"}) private String description;
@Validate(required=true, on="delete") private boolean confirm;
|
The list can also be inverted by supplying event names prefixed with '!'s, for example:
@Validate(required=true, on="!delete") private String description;
@Validate(required=true, on="delete") private boolean confirm;
|
Note: you cannot mix event names with and without '!'s for the same property, e.g. {"!delete", "save"}.
@Validate(minlength=##)
Specifies
the minimum length that the pre-conversion String must be. For example
you might specify that a password field must have a minimum length of 6.
Doing so would require the password to be six characters in length or
greater. If a field fails the minimum length test a single validation
error is generated. The default scope is validation.minlength and the error name is valueTooShort. This results in an attempt to find error messages in the resource bundle with the following keys:
- actionPath.fieldName.valueTooShort
- actionPath.fieldName.errorMessage
- fieldName.valueTooShort
- fieldName.errorMessage
- actionPath.valueTooShort
- validation.minlength.valueTooShort
The
error message is supplied with an additional parameter (which can be
used as {2} in the message text) which is the minimum length constraint
that was violated - e.g. 6 in the example above.
@Validate(maxlength=##)
Specifies
the maximum length that the pre-conversion String may be. For example
you might specify that a username field may have a maximum length of 25.
Doing so would require the username to be twenty-five characters in
length or less. If a field fails the maximum length test a single
validation error is generated. The default scope is validation.maxlength and the error name is valueTooLong. This results in an attempt to find error messages in the resource bundle with the following keys:
- actionPath.fieldName.valueTooLong
- actionPath.fieldName.errorMessage
- fieldName.valueTooLong
- fieldName.errorMessage
- actionPath.valueTooLong
- validation.maxlength.valueTooLong
The
error message is supplied with an additional parameter (which can be
used as {2} in the message text) which is the maximum length constraint
that was violated - e.g. 25 in the example above.
@Validate(minvalue=##)
Specifies
the minimum value for a numeric type (after it has been converted to a
number). For example you might specify an age field has a minvalue of 0
to enforce non-negative ages! If a field fails the minimum value test a
single validation error is generated. The default scope is validation.minvalue and the error name is valueBelowMinimum. This results in an attempt to find error messages in the resource bundle with the following keys:
- actionPath.fieldName.valueBelowMinimum
- actionPath.fieldName.errorMessage
- fieldName.valueBelowMinimum
- fieldName.errorMessage
- actionPath.valueBelowMinimum
- validation.minvalue.valueBelowMinimum
The
error message is supplied with an additional parameter (which can be
used as {2} in the message text) which is the minimum value constraint
that was violated - e.g. 0 in the example above.
@Validate(maxvalue=##)
Specifies
the maximum value for a numeric type (after it has been converted to a
number). If a field fails the maximum value test a single validation
error is generated. The default scope is validation.maxvalue and the error name is valueAboveMaximum. This results in an attempt to find error messages in the resource bundle with the following keys:
- actionPath.fieldName.valueAboveMaximum
- actionPath.fieldName.errorMessage
- fieldName.valueAboveMaximum
- fieldName.errorMessage
- actionPath.valueAboveMaximum
- validation.maxvalue.valueAboveMaximum
The
error message is supplied with an additional parameter (which can be
used as {2} in the message text) which is the maximum value constraint
that was violated.
@Validate(mask=".*")
Specifies a regular expression that the user's input must match. The mask is compiled using java.util.regex.Pattern
- if you are not familiar with regular expressions, the documentation
for the Pattern class provides a good introduction to the syntax. The
entire String submitted must match the mask/pattern supplied, as
specified by Pattern.matcher(input).matches().
If the input does not match the mask, a single validation error is output with the default scope validation.mask and the error name valueDoesNotMatch. This results in an attempt to find error messages in the resource bundle with the following keys:
- actionPath.fieldName.valueDoesNotMatch
- actionPath.fieldName.errorMessage
- fieldName.valueDoesNotMatch
- fieldName.errorMessage
- actionPath.valueDoesNotMatch
- validation.mask.valueDoesNotMatch
@Validate(expression="${true}")
Specifies
an expression using the JSP Expression Language (EL) to be evaluated.
Expressions must return a boolean value. Expression validation is run
after all form properties (that have passed validation so far) have been
bound into the ActionBean. As a result expressions have access to all
the values in the ActionBean!
The value being validated is also
available (of course) under the name 'this'. Other named values in the
expression are resolved first by looking for an ActionBean property of
the same name, and if one is not found, then by then a value is looked
for in the request scope, and then the session scope. All the usual EL
builtins are available, so it is also possible to access the request
parameters etc.
Care should be taken because, while expression
validation will not be executed on null or empty values, other values
referenced by the expression could still be null. Given the EL's
penchant for safely handling nulls, and converting types as needed, this
may lead to unexpected outcomes!
Some examples are in order:
@Validate(expression="this % 2 == 1") private int oddNumber;
@Validate(expression="oddNumber != null && this > oddNumber") private long biggerNumber;
@Validate(expression="context.user.name=='root' || this==context.user.name") private String name;
|
If the expression does not evaluate to true an error is created with the default scope validation.expression and the error name valueFailedExpression. This results in an attempt to find error messages in the resource bundle with the following keys:
- actionPath.fieldName.valueFailedExpression
- actionPath.fieldName.errorMessage
- fieldName.valueFailedExpression
- fieldName.errorMessage
- actionPath.valueFailedExpression
- validation.expression.valueFailedExpression
@Validate(converter=PercentageTypeConverter.class)
Specifies the TypeConverter
that should be used to convert this field. This is not usually
necessary as in most cases Stripes will use the right converter for the
target type. Two common cases where this is useful are:
- When
you are using formats that modify the meaning of the data. E.g.
formatting numbers as percentages causes 0.95 to be written as 95%.
Using the default Double/Float type converter in this case would result
in the value being multipled by 100 every time the user edited the
value. In this case you could tell Stripes to use the
PercentageTypeConverter which does the right thing.
- When you are converting to custom types, and have decided not to write your own (very simple) TypeConverterFactory. More on this in the next section.
Since this is not strictly a validation, but a way of modifying the type conversion, it does not produce any error messages.
Type Conversion
Type
conversion is quite simply the process of converting the String
parameters that are supplied in the HttpServletRequest into the types of
the corresponding ActionBean properties. The TypeConverter
interface specifies how Stripes will interact with classes that perform
type conversion. There are type converters in Stripes for most common
types in Java, and adding support for additional types is as easy as
coding up a new TypeConverter.
Stripes gets to know about TypeConverters in one of two ways. If an @Validate annotation specifes a converter then that converter will be used, no questions asked. Otherwise Stripes will ask the configured TypeConverterFactory to supply the appropriate TypeConverter for the type being converted to. Extending the DefaultTypeConverterFactory is fairly straight-forward. See the Configuration Reference for details on how to configure Stripes to use your custom TypeConverterFactory implementation.
The following sections document the individual type converters used in Stripes.
BooleanTypeConverter
The BooleanTypeConverter
determines that a value is "true" if it matches (ignoring case) any of
the values "true", "t", "yes", "y" or "on" or if the value can be parsed
as a number and does not equal zero.
Please note that Stripes
does not convert and bind parameters submitted in the request with the
value "" (empty string). So while you might think that "" would convert
to false, in fact conversion is never run, so the value on your
ActionBean will be whatever default value is assigned to the property.
Since any String can be converted to a boolean (either false or true), no validation errors are produced.
DateTypeConverter
The DateTypeConverter employs a number of techniques to get the input to parse as a java.util.Date. If the input cannot be parsed into a Date then a single validation error is produced. The default scope is converter.date and the error name is invalidDate.
EmailTypeConverter
The EmailTypeConverter, strictly speaking is not a type converter. It uses JavaMail to parse the String and return a well formatted email address as a String.
A single validation error is produced if the address cannot be parsed. The default scope is converter.email and the error name is invalidEmail.
EnumeratedTypeConverter
The EnumeratedTypeConverter
converts Strings representing the names of enumerated type values, back
into an instance of the enumerated type. If the value supplied does not
match any of the values of the target enumerated type, a single
validation error is produced. The default scope is converter.enum and the error name is notAnEnumeratedValue.
Number TypeConverters
Stripes
supplied a set of converters for converting to the Java numeric types:
byte, short, int, long, float, double and their corresponding wrapper
types as well as BigDecimal and BigInteger. They all operate similarly
(using one or more java.text.NumberFormats).
The converters make a best effort to parse a String as a number. They
can handle the following without any additional effort:
- additional white space
- currency and non-currency numbers
- use of the minus sign and parentheses to indicate negation
- use of grouping characters and decimals (obviously)
The
number TypeConverters produce two types of error. The first error is
common to all the number TypeConverters; it is generated when the String
is not parsable as a number. The default scope is converter.number and the error name is invalidNumber.
The
second error is named differently for each number TypeConverter, but
means the same; it is generated when the supplied number is out of the
range for the target type. The default scope is converter.[byte|short|integer|float] and the error name is outOfRange.
For all outOfRange errors two additional parameters ({2} and {3}) are
supplied to the error message. The parameters are the minimum and
maximum allowable values for the type being converted to, e.g.
Integer.MIN_VALUE and Integer.MAX_VALUE.
PercentageTypeConverter
The PercentageTypeConverter
converts numbers displayed as percentages (e.g. 95%) into decimal
numbers (e.g. 0.95). It requires that the target type is either float, double (or the wrapper types) or BigDecimal. It produces the same error as other number classes, with a default scope of converter.number and the error name invalidNumber.
OneToManyTypeConverter
The OneToManyTypeConverter
is a special type converter that converts a single value in the request
into a List of values to be bound to the ActionBean. For example, if a
user were to enter one or more numbers into a single field, '123, 456
789' that is converted using the OneToManyTypeConverter, a List would be bound to the ActionBean containing three numbers.
The
type of the elements in the List is determined based on the declared
type in the ActionBean, so if you declare List<Long> you'll get
Longs, if you declare List<Boolean> you'll get Booleans. Under the
covers the OneToManyTypeConverter will request the
appropriate type converter for the individual items, and hence will
generate the same validation errors as the LongTypeConverter, BooleanTypeConverter etc.
Custom Validation
Custom
validation refers to the execution of arbitrary, custom, validation
logic within the ActionBean. ActionBean classes may optionally include
one or more validation methods. Validation methods are regular methods
that are marked with the annotation @ValidationMethod.
These methods are invoked after all other validations have been run and
all values have been converted and bound to the ActionBean.
By
default validation methods will not be invoked if preceeding validations
resulted in errors. Since the ActionBean will be fully populated when
validation methods are called it is safe to refer to any property of the
ActionBean, or through the ActionBeanContext access values in the
HttpServletRequest (though hopefully this will not be common since it
makes the ActionBean less testable).
It is possible to configure
Stripes to, by default, invoke validation methods regardless of whether
or not validation errors exist. To do this refer to the Configuration Reference.
Note that by doing this you are no longer guaranteed that all required
fields have been supplied or that type conversions and binding succeeded
before validation methods are called.
The @ValidationMethod
annotation has three attributes that allow the application of the
method to be altered, all of which are completely optional. The first is
the 'when' attribute which allows the developer to specify whether or
not the method should be run when errors exist, regardless of the
application default policy. E.g.
@ValidationMethod(when=ValidationState.ALWAYS) public void validateSomething { ... }
@ValidationMethod(when=ValidationState.NO_ERRORS) public void validateSomethingElse { ... }
|
The 'on' attribute (similar to the 'on' attribute of @Validate)
restricts the method to run only on certain events. By default
validation methods are run without regard to the event being handled.
The 'on' attribute can specify a single event, a list of events, or one
or more events preceeded by '!'s. In the last case the meaning is
inverted, and the method is executed on all events except those listed.
E.g.
@ValidationMethod(on="create") public void one() { ... }
@ValidationMethod(on="update") public void two() { ... }
@ValidationMethod(on={"update","delete"}) public void three() { ... }
@ValidationMethod(on="!delete") public void four() { ... }
|
The 'priority' attribute is useful only when multiple
validation methods exist within an ActionBean and the order in which
they are executed is important. Methods with lower (nearer to zero)
values for the 'priority' attribute are run before those with higher
values.
Lastly, it should be noted that there are two ways to get access to the ValidationErrors
object in order to save validation error messages. Firstly, as with any
other method in the ActionBean, validation methods can call getContext().getValidationErrors()
to retrieve the validation errors. However, they may also take the
ValidationErrors objects as a parameter, in which case it will be
supplied to the method. E.g.
@ValidationMethod public void validateSomething(ValidationErrors errors) { ... }
|
Lastly it should be noted that validation methods must be
public, must take either zero parameters or one parameter of type
ValidationErrors and may return any type, though returns will be
ignored.
Sometimes
it's just not possible (or reasonable) to validate absolutely
everything up front in validation methods. Maybe you don't have all the
data you need, maybe you won't know if something really is valid until
you try and do it! For example, you might not be able to validate that a
withdrawal from a bank account is valid until you do it due to
concurrency issues (what if two threads check a $100 balance then allow
$75 withdrawals?).
In these situations you can create validation
errors in your handler method, and send the user back to the page they
came from (or elsewhere if you choose). The following is an example:
@HandlesEvent("Withdrawal")
public Resolution withdrawFunds() {
try {
getAccount().withdraw( getAmount() );
return new RedirectResolution("/account/summary.jsp");
}
catch (InsufficientBalanceException ibe) {
ValidationErrors errors = new ValidationErrors();
errors.add( "amount", new LocalizableError("/account/transaction.action.insufficientFunds") );
getContext().setValidationErrors(errors);
return getContext().getSourcePageResolution();
}
}
|
Selectively Disabling Validation
Often
is it desirable to have validation run for one or more events in an
ActionBean, but not for others. For example the "save" method might
require validation, as might the "add" method. But the "prepopulate"
method might not. While usage of the 'on' attribute of @Validate and @ValidationMethod can acheive this, sometimes it's overly repetitive.
Stripes uses the @DontValidate annotation to make this easier. Any handler method that is annotated with @DontValidate
will be invoked directly skipping validation. Type conversion and
binding are still executed, and may produce validation errors. However,
it is expected that in most (all?) cases the @DontValidate annotation will be used with events whose only input is not user-input (e.g. hidden fields, select lists etc.).
What Happens When There Are Errors?
When one or more validation errors are registered during a request, Stripes will execute the Resolution returned by calling ActionBeanContext.getSourcePageResolution(). By default this returns a ForwardResolution
which will forward the request to the same JSP page from which it
originated. The page is identified by a request parameter called _sourcePage which is written as a hidden field by the Stripes form tag, and included as a parameter by the Stripes link tag.
To change this behaviour at the global level it is possible to simply override the ActionBeanContext.getSourcePageResolution() method to return a different resolution.
Changing Validation Outcomes
There
are times when even the most flexible system cannot give you
everything, and you want to manage the validation process at a more
detailed level. You can do this by implementing the ValidationErrorHandler interface in your ActionBean. The interface defines a single method handleValidationErrors(ValidationErrors errors) that is invoked when validation fails, but before determining what to do next.
The ActionBean can manipulate the ValidationErrors
in any way it sees fit and the changes will be taken into account
before proceeding. If all errors are cleared out, it will be as if they
never existed! The ActionBean can also use this opportunity to undo any
side-affects that might be caused by the binding process if necessary,
or to substitute a different Resolution. Check out the ValidationErrorHandler javadoc for more information.
Stripes
provides field meta data which can be used for client side validation
and layout. Here is an example provided by Aaron Porter, using the
jQuery JavaScript framework:
(http://article.gmane.org/gmane.comp.java.stripes.user/7575)
(Required scripts available at http://stripes-stuff.svn.sourceforge.net/viewvc/stripes-stuff/JavaScript/trunk/scripts/jquery/)
<s:field-metadata var="fieldMetadata">
$(function(){applyStripesValidation('${fieldMetadata.formId}', ${fieldMetadata});});
</s:field-metadata>
|
The field-metadata tag provides meta data
for fields referenced by input tags. The tag can only provide meta data
for fields referenced by tags that appear before its own position in the
page code, so usually you'll put it at the end of your form, just
before the closing form tag. (If you put it just below opening form tag,
for example, no field meta data will be available.) Also, if you have
more than one form on your page, be sure to set the form id yourself,
otherwise you'll end up with a duplicated form id on your page and
validation will only work for one of them.
The tag binds a block of Javascript (actually, JSON) code to a JSP variable, and generates an HTML script
block around its body. The body therefore should be Javascript code
that makes use of the meta data. Here is also an example of how to use
the tag with a Jquery validation plugin:
(http://machak.com/code.html)
Note: visit the above link for updates and improvements. The code below is a skeletal example for illustration:
(function($){
$.fn.stripesValidation = function(formId, metaData) {
var form = $('#' + formId);
var mask_count = 0;
for (var fieldName in metaData) {
var field = form.find('[name=' + fieldName + ']');
addValidation(field, metaData[fieldName]);
form.validate();
}
function addValidation(field, obj) {
for (var prop in obj) {
debug(prop);
switch(prop){
case 'label':
break;
case 'required':
if (obj[prop]) {
field.addClass(prop);
}
break;
case 'minlength':
field.attr(prop, obj[prop]);
break;
case 'maxlength':
field.attr(prop, obj[prop]);
break;
case 'mask':
setMask(field, obj[prop]);
break;
case 'typeConverter':
setConverter(field, obj[prop]);
break;
default:
debug('missing this:' + prop + ':' + [obj[prop]]);
}
}
}
function setMask(field, mask) {
mask_count++;
var methodRef = 'maskMethod' + mask_count;
field.addClass(methodRef);
$.validator.addClassRules({
methodRef: {
methodRef: true
}
});
$.validator.addMethod(methodRef, function (value) {
return mask.test(value);
}, 'Value is invalid');
}
function setConverter(field, converter) {
if (converter == 'EmailTypeConverter') {
field.addClass('email');
}
else {
debug('missing converter mapping:' + converter);
}
}
function debug(msg){
if (window.console && window.console.log) {
window.console.log(msg);
}
}
};
})(jQuery);
|
Shlomo Konwisser
From the code in v1.5.7 (
LocalizedUtility.getLocalizedFieldName(...)), I see that the resource naming "actionPath.fieldName" is deprecated.There is a new way not mentioned in this document which is probably meant to be used instead: "fullActionBeanNameWithPackagePath.fieldName".
Example of the new resource naming
Assuming my action bean class is "UserManagerActionBean.java", located in package "com.mycompany.user". Let the field name of interest be "userNickname". The expected resource definition in
StripesResources.propertieswould be:com.mycompany.user.UserManagerActionBean.userNickname=How do you want to be called?